已经有点感觉用ioc container来说明co不见得是个好主意了。 这个container的例子举出来,明显提出意见的人比那个简单的logging例子少了很多。 毕竟连pico是怎么回事,怎么用,很多人都还不见得了了。更不提多少人对pico的用法就是一个很in的fancy factory。买椟还珠。
不过,既然开始了,让我还是有始有终吧。
这章还是让我们看看co的refactor。
其实,很多人问:怎样把握co里面的基本组合子的度;什么样的组合子算是基本;怎样做到正交;多少的基本组合子才算够用;怎么知道这个组合子会被用到等等。
其实,答案都来自重构。
没有谁一下子就作对的。co比起oo,我感觉在设计上反而更容易避免过度设计。
为什么?
设计oo的时候,你要分析需求,设计各个模块的通信接口,这个过程,同样需要经验,同样需要摸索,同样没有一踀而就的捷径。
但是,oo设计的时候又要避免过度,一些时候,在是否通过接口预留灵活性,提取容易变化的部分,或者是尽量简单之间,还是有冲突的。你需要做一个艰难的猜测和抉择。 而一旦抉择作出,以后如果发现事情进展不如所愿,那么改动接口的代价相当的大。
而如果使用co,在设计简单的各个组合子的时候,你会以一种非常渐进式的方式来发现:哦,原来的组合子设计不够正交,有这个地方可以抽出来,好,抽出来,把波及到的几个组合子的设计修改一下。
因为组合子都非常简单,这个变化的波及范围一般来说相当小。
好,空话少说,我们还是看具体例子。
现在,我们发现,除了withArgument, withProperty,我们还希望更灵活地设置参数,比如,我们希望说: 对组件X的各个参数,类型为A的,选取以"a1"标识的组件作为参数值,其它的按照缺省方式。 对组件Y的各个参数,类型为A的,选取以"a2"标识的组件为参数值,其它的按照缺省方式。
这个需求有几个点: 1。需要能够通过key来直接指定某个某个组件,相当于一个"ref"。 2。需要对参数配置有除了按照参数位置之外的更灵活的配置(比如,按照参数类型)。
对第一点,我们制作以下的组合子来对应。(看,我们是可以随着需求虽然丰富我们的基本组合子的集合的) 我们期望做一个UseKey组合子,它可以从容器里面取得另外一个用某个key标识的组件,然后把一切动作都delegate过去。
java代码:
class UseKey extends Component{ private final Object key; public Object create(Dependency dep){ //????????? } .... }
可是,一开始写代码,就发现,这个代码写不下去!我们需要得到这个容器,才能从这个容器里面取得那个要delegate的组件。可是这个可爱容器对象在哪里呀?
仔细分析下来,发现,没有办法。唯一的办法是修改Dependency接口,让它除了帮助解析参数和property之外,再提供给我们当前容器的信息。
Dependency接口变为:
java代码:
interface Dependency{ Object getArgument(int i, Class type); Object getProperty(Object key, Class type); Container getContainer(); }
Wow!要改接口了!其实,这一点也不可怕。为什么?
co还有另外一个优点我们一直没有提及:细节封装。这个封装不是一般OO意义上的封装,而是说:把要实现的接口细节封装起来,让客户通过预定义好的组合方式来扩展,而不是象oo那样让用户实现实现这个接口来扩展。
其实,如果用户使用的都是Component对象,而创建Component对象都是通过: java代码:
Container.getInstance(Object key);
这种方式,那么,Dependency这个接口已经实际上沦为我们的内部实现细节了。用户根本不需要知道存在这么一个接口。 实际上,当我们的组合子足够丰富之后,完全可以把Dependency接口隐藏在包内部,彻底地对用户屏蔽这个接口。 如此,客户的扩展完全通过组合Component对象,而不是实现Component接口并且调用Dependency接口。 不管这个Dependency接口是如何设计的,如何变化,我们都可以把变化隔离在我们包内部,而不会影响用户。
好吧。现在假设我们修改了Dependency接口,那么UseKey可以被写为:
java代码:
class UseKey extends Component{ private final Object key; public Object create(Dependency dep){ final Component c = dep.getContainer().getComponent(key); if(c==null)throw new ComponentNotFoundException(...); return c.create(dep); } .... }
然后,更灵活的参数配置。对这个,我们可以借鉴bind操作,做一个对参数的bind。
java代码:
interface ParameterBinder{ Component bind(int i, Class type); }
不知道你从Binder接口和ParameterBinder接口看出点什么没有? 1。Binder, ParameterBinder接口都是给用户去实现的。 2。这两个接口都不暴露Component的细节,它们的参数和返回值都不涉及Component的接口签名,客户在实现这两个接口的时候,完全不必关心象Dependency接口这种细节。 3。返回值都是Component,这样,所有的Component组合子都可以被自由使用。
实际上,monad组合子就是通过这种方式来在高阶逻辑的层次上隐藏底层细节。
java代码:
class ParameterBoundDependency implements Dependency{ private final Dependency dep; private final ParameterBinder binder; public Object getArgument(int i, Class type){ return binder.bind(i, type).create(dep); } ... }
java代码:
ParameterBoundComponent extends Component{ private final Component c; private final ParameterBinder binder; public Object create(Dependency dep){ return c.create(new ParameterBoundDependency(dep, binder)); } ... }
用ParameterBinder来做一个Dependency的decorator,问题得到了解决。
然后我们来使用ParameterBoundComponent,为了书写简便,我们假设Component类有一个函数叫做bind (ParameterBinder binder)。另外Components类有一个useKey(Object key)函数来生成一个Component对象,用来指向容器内的另外一个组件。
于是,上面的需求被实现为:
java代码:
Component x = ...; Component x2 = x.bind(new ParameterBinder(){ public Component bind(int i, Class type){ if(type.equals(A.class)){ return Components.useKey("a1"); } else{ //???? 行1 } } });
这个x2组件,就是为了实现“当参数类型为A,使用a1,否则使用缺省方式”。 可是,在行1处,再次遇到了障碍。这个所谓的“缺省方式”,怎么表示? 。
经过思考,我们决定实现一个useArgument(int i, Class type)这样一个组合子,这个组合子可以主动在当前的Dependency对象中选择某个参数作为自己的值。这样,上面的行1就可以写作: java代码:
Component x = ...; Component x2 = x.bind(new ParameterBinder(){ public Component bind(int i, Class type){ if(type.equals(A.class)){ return Components.useKey("a1"); } else{ return Components.useArgument(i, type);// 行1 } } });
下面来实现一个UseArgument类:
java代码:
class UseArgument extends Component{ private final int i; private final Class type; public Object create(Dependency dep){ return dep.getArgument(i, type); } ..... }
哈。完美。一切仍然尽在掌握。 我们可以以几乎任何方式来customizer组件的参数和property。
实际上,如果我们回头看看,甚至可以发现,withArgument(int i, Class type)完全可以用bind(ParameterBinder)来重写: java代码:
Component withArgument(Component c, final int i, final Component arg){ return c.bind(new ParameterBinder(){ public Component bind(int k, Class type){ if(k==i) return arg; else return Components.useArgument(k, type); } }); }
我们很开心地看到,原来的WithArgument类,WithProperty类都可以扫进垃圾箱了。我们只需要实现更加简单的ParameterBinder接口就可以搞定一切。哈。
同时,希望你也看到了隐藏这些具体的WithArgument,ValueComponent类,而用静态工厂函数withArgument(), value()来代替的好处: 我们可以自由地重构。当发现某个组合子本身并非最简单,而是可以从一些更简单的组合子推演出来,我们只需要改动这些静态工厂函数,而不必告诉用 户:对不起,我的设计改了,不想要WithArgument类了,你能不能改改你的那段new WithArgument(...)的代码?
co让用户只关注接口,而不要管某个功能是直接实现的,还是组合出来的。静态工厂函数提供了对这个细节的封装。
另外一个也许会比较常见的需求,是用一个数组来一次性指定某个组件的所有参数,比如:
java代码:
c.withArguments(new Component[]{c1, c2, c3});
这个功能用bind非常非常好实现:
java代码:
Component withArguments(final Component[] args){ return bind(new ParameterBinder(){ public Component bind(int i, Class type){ return args[i]; } }); }
当然,你还可以举一反三地提出很多其它的定制参数和property的方法。
好了。今天就到这里。在结束前,我来先提出两个新的需求: 1。希望对一些用到Logger对象的类注射Logger实例,而这个Logger实例需要用这个使用Logger对象的类对象来创建,这样,这个Logger对象可以静态地知道谁在使用它,而不必每次都构造一个异常来取得StackTrace。 比如, java代码:
new ClassX(..., Loggers.instance(ClassX.class), ...);
怎样在容器级别全局地规定这个规则呢?我们不知道哪些组件需要注射Logger,也不知道这些组件在哪个参数注射Logger对象。
2。怎样提供缺省参数?这样,如果某个参数的需要可以在容器中解析,则拥这个解析出来的实例,否则,使用一个缺省组件。
在下一节,我们会通过这两个例子来继续解释co的重构过程。